/*
 * Copyright (c) 2012 Mikaël GUILLEMOT
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package org.g2de.display;

import java.awt.Canvas;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.RenderingHints;
import java.awt.image.BufferStrategy;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * Classe generique de moteur graphique permettant de gerer : </br> - la methode
 * de rendu</br> - la resolution </br> - les fps</br> - l'anti aliasing</br> -
 * les couches d'affichage (layers)</br> Une utilisation courante peut se faire
 * comme suit:
 * 
 * <pre>
 * engine.setRenderingMode(Engine2D.SOFTWARE_RENDER);
 * engine.setResolution(640, 480);
 * engine.setFPSLimit(Engine2D.NO_FPS_LIMIT);
 * engine.drawFPS(true);
 * engine.useAntiAliasing(false);
 * engine.initLayers(10);
 * engine.getCanvas().addMouseListener(...);
 * ...
 * engine.validate();
 * </pre>
 * 
 * 
 * @author Mikael GUILLEMOT
 * @version 1.0
 * @since 1.0
 * 
 */
public class Engine2D extends Thread {

	public final static int SOFTWARE_RENDER = -1;
	public final static int HARDWARE_DOUBLE_BUFFER = 2;
	public final static int HARDWARE_TRIPLE_BUFFER = 3;
	public final static int NO_FPS_LIMIT = 0;

	public static int X_SIZE = 640;
	public static int Y_SIZE = 480;

	private boolean antialiasing = false;
	private boolean isrunning = false;
	private boolean drawFps = true;
	private boolean paintItBlack = false;
	private int capFps = -1;
	private int sleepTime = 0;
	private int fps = 0;
	private int fpsSum = 0;
	private int fpsGroup = 0;
	private int averageFps = 0;
	private long t1 = 0, t2 = 0;

	private GraphicsEnvironment env = null;
	private GraphicsDevice device = null;
	private GraphicsConfiguration gc = null;
	private BufferStrategy bufferStrat = null;
	private Dimension res = null;
	private Canvas canva = null;
	private volatile List<Renderable>[] layers;

	/**
	 * constructeur : prepare un environement de base qui peut etre configuré
	 * avant utilisation.
	 */
	public Engine2D() {
		res = new Dimension(X_SIZE, Y_SIZE);
		env = GraphicsEnvironment.getLocalGraphicsEnvironment();
		// LATER ici on peu fair en sorte que l'utilisateur force un ecran donné
		device = env.getDefaultScreenDevice();
		gc = device.getDefaultConfiguration();
	}

	/**
	 * Initialise le nombre de layers.<br>
	 * lors du rendu, la liste des layers est parcourue dans l'ordre (i.e. le
	 * layer 1 est le premier dessiné).<br>
	 * Les listes de rendu sont reinitialisées lors de l'appel a cette methode.
	 * Le SuppressWarnings correspond a l'impossibilité de faire new
	 * List<Renderable>[number]
	 * 
	 * @param number
	 *            le nombre de layer a prendre en charge.
	 */
	@SuppressWarnings("unchecked")
	public void initLayers(int number) {
		// en cas de reinitialisation, on vide et on de-aloue
		if (layers != null) {
			clearDisplay();
			for (int i = 0; i < layers.length; i++)
				layers[i] = null;
			layers = null;
		}
		// on cree la nouvelle structure
		layers = new List[number];
		for (int i = 0; i < layers.length; i++)
			layers[i] = new ArrayList<Renderable>(100);
	}

	/**
	 * Supprime tous les objets contenu dans le layer
	 * 
	 * @param layerIndex
	 *            le numero du layer
	 */
	public void clearLayer(int layerIndex) {
		if (layerIndex < layers.length)
			layers[layerIndex].clear();
	}

	/**
	 * vide tout les layers
	 */
	public void clearDisplay() {
		for (int i = 0; i < layers.length; i++)
			clearLayer(i);
	}

	/**
	 * Enregistre le Renderable "object" dans la liste de rendu du layer
	 * 
	 * @param object
	 * @param layerIndex
	 */
	public void register(Renderable object, int layerIndex) {
		layers[layerIndex].add(object);
		// if(sortAvtive)TODO verifier le classement, option de classement
		Collections.sort(layers[layerIndex]);
	}

	/**
	 * Supprime le Renderable "object" dans la liste de rendu du layer
	 * 
	 * @param object
	 * @param layerIndex
	 */
	public void remove(Renderable object, int layerIndex) {
		layers[layerIndex].remove(object);
	}

	/**
	 * initialise les buffers d'affichage. La zone d'affichage doit etre visible
	 * pour que cela se passe bien.
	 * 
	 * @param nb_buffers
	 *            peut etre choisi parmis SOFTWARE_RENDER,HARDWARE_DOUBLE_BUFFER
	 *            ou HARDWARE_TRIPLE_BUFFER
	 */
	public void setRenderingMode(int mode) {
		if (mode == HARDWARE_DOUBLE_BUFFER || mode == HARDWARE_TRIPLE_BUFFER) {
			canva.createBufferStrategy(mode);
			bufferStrat = canva.getBufferStrategy();
		} else
			bufferStrat = null;
	}

	/**
	 * initialise la resolution de l'espace de rendu (640*480 par defaut)
	 * 
	 * @param x
	 * @param y
	 */
	public void setResolution(int x, int y) {
		// TODO a revoir pour modifier la resol X_ et Y_
		res = new Dimension(x, y);
		canva.setSize(res);
	}

	/**
	 * initialise l'anti aliasing (desactivé par defaut)
	 * 
	 * @param activated
	 */
	public void useAntiAliasing(boolean activated) {
		antialiasing = activated;
	}

	/**
	 * initialise l'affichage des FPS (desactivé par defaut)
	 * 
	 * @param activated
	 */
	public void drawFPS(boolean activated) {
		drawFps = activated;
	}

	/**
	 * initialise la limitation de fps (aucune par defaut)
	 * 
	 * @param limit
	 *            nombre de fps a ne pas depasser
	 */
	public void setFPSLimit(int limit) {
		capFps = limit;
	}

	/**
	 * valide l'initialisation du moteur. Cette methode DOIT etre appelé pour
	 * que le rendu actif puisse fonctionner.
	 * 
	 * @return false en cas de problemes de config false sinon
	 */
	public boolean validate() {
		if (canva == null)
			return false;
		if (bufferStrat == null)
			if (canva.getGraphics() == null)
				return false;
		isrunning = true;
		return true;
	}

	/**
	 * @return le canvas. Le cree en cas de besoin
	 */
	public Canvas getCanvas() {
		if (canva == null) {
			canva = new Canvas(gc);
			canva.setIgnoreRepaint(true);
			canva.setSize(res);
		}
		return canva;
	}

	@Override
	public void run() {
		if (bufferStrat == null)
			SoftwareDisplay();
		else
			hardwareAcceleratedDisplay();
	}

	// methode d'affichage avec buffer hard (gestion auto)
	private void hardwareAcceleratedDisplay() {
		Graphics2D buffer = null;
		while (isrunning) {
			try {
				// on demare le decopte de temps pour l'affichage de l'image
				fps = (int) (1000 / getTime());
				timerStart();
				// on recupere un buffer, on le prepare
				buffer = (Graphics2D) bufferStrat.getDrawGraphics();
				setRenderingHint(buffer);
				// on dessine les element enregistré
				display(buffer);
				// on finalise avec les FPS si demandé
				CapAndDisplayFPS(buffer);
				// TODO SCALING live ou pas?
				// on affiche le buffer
				if (!bufferStrat.contentsLost())
					bufferStrat.show();
				waitNextDraw();
			} finally {
				// on libere le buffer,
				if (buffer != null)
					buffer.dispose();
				// on arret le decompte de temps pour l'image
				timerStop();
			}
		}
	}

	// methode d'affichage avec buffer soft (ou acceleré par la vm)
	private void SoftwareDisplay() {
		Graphics display = canva.getGraphics();
		// on prepare un buffer software
		BufferedImage imageBuffer = new BufferedImage(res.width, res.height, BufferedImage.TYPE_4BYTE_ABGR);
		Graphics2D buffer = imageBuffer.createGraphics();
		setRenderingHint(buffer);
		while (isrunning) {
			fps = (int) (1000 / getTime());
			timerStart();
			display(buffer);
			// on finalise avec les FPS si demandé
			CapAndDisplayFPS(buffer);
			// on affiche le buffer
			display.drawImage(imageBuffer, 0, 0, canva);
			// TODO SCALING
			waitNextDraw();
			timerStop();
		}
	}

	private void setRenderingHint(Graphics2D buffer) {
		buffer.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_SPEED);
		buffer.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
		buffer.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
		if (antialiasing)
			buffer.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
		buffer.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_OFF);
	}

	private void CapAndDisplayFPS(Graphics2D buffer) {
		if (capFps > 0) {
			if (fps < capFps && sleepTime > 0)
				sleepTime--;
			if (fps > capFps)
				sleepTime++;
		}
		if (drawFps) {
			if (fpsGroup > 0) {
				fpsSum += fps;
				fpsGroup--;
			} else {
				if (capFps > 0)
					fpsGroup = capFps;
				else
					fpsGroup = 100;
				averageFps = fpsSum / fpsGroup;
				fpsSum = 0;
			}
			buffer.setColor(Color.BLUE);
			buffer.drawString(Integer.toString(averageFps), 5, 14);
			buffer.setColor(Color.WHITE);
			buffer.drawString(Integer.toString(averageFps), 4, 13);
		}
	}

	private void waitNextDraw() {
		if (sleepTime < 1)
			Thread.yield();
		else
			try {
				Thread.sleep(sleepTime);
			} catch (InterruptedException e) {
			}
	}

	// renvoi le temps en ms
	private double getTime() {
		return ((t2 - t1) / 1000000.0);
	}

	private void timerStart() {
		t1 = System.nanoTime();
	}

	private void timerStop() {
		t2 = System.nanoTime();
	}

	// calcul de l'image a afficher
	private void display(Graphics2D buffer) {
		// on vide le buffer
		buffer.setColor(Color.WHITE);
		if (paintItBlack)
			buffer.setColor(Color.BLACK);
		buffer.fillRect(0, 0, res.width, res.height);
		// on affiche
		for (List<Renderable> renderlist : layers)
			for (Renderable r : renderlist)
				r.display(buffer);
		// TODO if()
		buffer.setColor(Color.BLACK);
		buffer.drawRect(0, 0, res.width - 1, res.height - 1);
	}
	
	/**
	 * Arret le thread d'affichage en vue de l'extinction du programme
	 */
	public void exit() {isrunning = false;}
}
